Debugging en 4K: Cómo domar el caos de objetos complejos con [DebuggerDisplay]
Introducción
Has llegado a esa línea del código. Pones un breakpoint. Te asomas al debugger, y ves esto:
Name Value Type
order {MyApp.Domain.Order} Order
Horrible. Para ver algo útil tienes que expandir el objeto, luego expandir Lines, luego cada línea... una pérdida de tiempo. La buena noticia: Visual Studio te da superpoderes con tres atributos mágicos.
[DebuggerDisplay] — el resumen en una línea
Este atributo controla el texto que aparece en la columna Value del debugger. Puedes usar { } para referenciar campos, propiedades o métodos.
[DebuggerDisplay("{Id}: {Customer} | {Status} | {Total:C}")]
public class Order
{
public int Id { get; set; }
public string Customer { get; set; }
public string Status { get; set; }
public decimal Total { get; set; }
}
Ahora ves:
Name Value Type
order #1001: Ana | Shipped | $1,245 Order
Mucho mejor.
Buenas prácticas: propiedad privada DebuggerDisplay
No pongas expresiones complejas directo en el atributo. Crea una propiedad privada y usa el sufijo ,nq (no quotes):
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Order
{
private string DebuggerDisplay =>
$"#{Id} | {Customer} | {Status} | {Lines?.Count ?? 0} items";
}
El sufijo ,nq evita que el string se muestre entre comillas. La evaluación es perezosa (solo se calcula cuando miras el objeto) y no ensucias la API pública.
[DebuggerTypeProxy] — una vista alternativa sin modificar la clase
Cuando expandes la flecha ▶, puedes controlar qué propiedades se muestran sin tocar la clase real. El proxy puede exponer info calculada que no existe en la clase original:
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(OrderDebugView))]
public class Order
{
public int Id { get; set; }
public string Customer { get; set; }
public DateTime CreatedAt { get; set; }
public List<OrderLine> Lines { get; set; }
public decimal Discount { get; set; }
public string Status { get; set; }
private string DebuggerDisplay =>
$"#{Id} | {Customer} | {Status} | {Lines?.Count ?? 0} items";
}
internal class OrderDebugView
{
private readonly Order _order;
public OrderDebugView(Order order) => _order = order;
public string Status => _order.Status;
public decimal Subtotal => _order.Lines?.Sum(l => l.Price * l.Quantity) ?? 0;
public decimal Discount => _order.Discount;
public decimal Total => Subtotal - Discount;
public int ItemsCount => _order.Lines?.Count ?? 0;
public TimeSpan Age => DateTime.Now - _order.CreatedAt;
}
Al expandir ves estas propiedades como si fueran parte de Order, pero el código real no cambia.
[DebuggerBrowsable(RootHidden)] — aplanar colecciones
Cuando tienes una lista y la expandes, normalmente ves [0], [1], etc. Con RootHidden ocultas la propiedad colección y muestras sus elementos directamente:
internal class OrderDebugView
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public OrderLine[] Items => _order.Lines?.ToArray();
}
Así los OrderLine aparecen directamente al expandir Order, sin el nivel intermedio Lines.
Caso real: objetos anidados a 3 niveles
Aquí está el escenario completo con Order → OrderLine → Product → Category:
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
private string DebuggerDisplay => $"[{Department}] {Name}";
}
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Product
{
public string Sku { get; set; }
public string Name { get; set; }
public Category Category { get; set; }
public decimal UnitPrice { get; set; }
private string DebuggerDisplay =>
$"{Name} | SKU: {Sku} | ${UnitPrice}";
}
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class OrderLine
{
public Product Product { get; set; }
public int Quantity { get; set; }
private string DebuggerDisplay =>
$"{Product?.Name} x{Quantity} = {Quantity * (Product?.UnitPrice ?? 0):C}";
}
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(OrderDebugView))]
public class Order
{
public int Id { get; set; }
public string Customer { get; set; }
public List<OrderLine> Lines { get; set; }
private string DebuggerDisplay =>
$"#{Id} | {Customer} | {Lines?.Count ?? 0} lines";
}
internal class OrderDebugView
{
private readonly Order _order;
public OrderDebugView(Order order) => _order = order;
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public OrderLine[] Items => _order.Lines?.ToArray();
}
Lo que ves en el debugger
Name Value Type
order #1001 | Ana | 2 lines Order
▶ [0] Laptop x2 = $2,400.00 OrderLine
▶ Product Product
▶ Category Category
Id: 5
Name: "Electronics"
Department: "Tech"
Sku: "LP-001"
Name: "Gamer Laptop"
UnitPrice: 1200
Quantity: 2
▶ [1] Mouse x1 = $45.00 OrderLine
▶ Product Product
▶ Category Category
...
Cada nivel se expande con su propio ▶, y cada uno ya te da la información clave sin tener que picarle.
⚠️ Advertencias importantes
- Rendimiento: Las expresiones se evalúan cada vez que el debugger pausa. No pongas operaciones costosas (accesos a BD, llamadas HTTP) dentro de
DebuggerDisplay. - Show raw structure: Si activas Tools > Options > Debugging > General > Show raw structure of objects in variables windows, todos estos atributos se ignoran.
- Solo C#/CLI: En código nativo C++, solo funciona en C++/CLI.
- Herencia: Si pones
[DebuggerDisplay]en una clase base, las subclases también lo heredan.
Bonus: DebuggerBrowsableState
| Estado | Comportamiento |
|---|---|
Collapsed |
Por defecto. La propiedad se muestra colapsada. |
RootHidden |
Oculta la propiedad raíz, muestra sus elementos hijos directamente. Ideal para colecciones. |
Never |
No muestra la propiedad en el debugger. |
Conclusión
Con solo tres atributos —DebuggerDisplay, DebuggerTypeProxy y DebuggerBrowsable— transformas la experiencia de debugging. De ver {MyApp.Domain.Order} pasas a tener resúmenes inteligentes, vistas alternativas y colecciones aplanadas.
La regla de oro: si tienes que expandir más de 2 niveles para entender el estado de un objeto, te falta un [DebuggerDisplay] en algún lado.
Este artículo se basa en contenido de Tools and Skills for .NET 8 (Mark J. Price), Systems Programming with C# and .NET (Vasko Sotirovski), y la documentación oficial de Microsoft sobre DebuggerDisplayAttribute.